﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace Muel.TypeScriptFacade.Core
{
    public class TypeScriptObjectManager
    {
        private TypeScriptServiceManager manager;
        public TypeScriptObjectManager(TypeScriptServiceManager manager)
        {
            this.manager = manager;
        }

        internal string GenerateKey(Type type)
        {
            if (type.IsGenericParameter)
            {
                return string.Format("{0}~{1}", type.Name, type.DeclaringType.FullName);
            }
            else if (string.IsNullOrEmpty(type.FullName))
            {
                return type.Name;
            }
            else
            {
                return type.FullName;
            }
        }

        internal Dictionary<string, TypeScriptType> cache = new Dictionary<string, TypeScriptType>();
        internal TypeScriptType Create(Type type)
        {
            string temp = type.Name;
            string key = this.GenerateKey(type);

            // return from the cache if the object definition has already been created
            if (cache.ContainsKey(key))
            {
                return cache[key];
            }

            if (!type.IsUnderlyingPrimitive() && !type.IsGenericParameter)
            {
                Type elementType = type;
                if (type.IsCollection())
                {
                    elementType = type.GetCollectionTypeArgument();
                }
                
                if (!elementType.IsEnum && 
                    !elementType.HasAttributeOfType<DataContractAttribute>() &&
                    !elementType.HasAttributeOfType<SerializableAttribute>())
                {
                    throw new UnexpectedObjectException(elementType);
                }
            }

            // instantiate the object, then build any required module objects
            TypeScriptType obj = TypeScriptType.Create(manager, type);

            if (obj.ShouldEmit)
            {
                TypeScriptModule module = this.GetModule(obj.Namespace);
                module.AddObject(obj);
            }

            cache.Add(key, obj);

            // initialise the inheritance hierarchy after adding to the cache to avoid an infinite loop
            obj.InitialiseInheritance();

            return obj;
        }

        internal TypeScriptType[] GetObjects()
        {
            return cache.Values.ToArray();
        }

        internal string ToTypeScript()
        {
            string output = string.Empty;

            // get the upper bound of the nesting
            int maxNestingDepth = (cache.Values.Count == 0) ? 0 : cache.Values.Max(o => o.NestingDepth);

            // go through and output the top level modules, which will recursively
            // create TypeScript for any child modules
            IEnumerable<TypeScriptModule> topLevelModules = GetTopLevelModules();
            for (int nestingDepth = 0; nestingDepth <= maxNestingDepth; nestingDepth++)
            {
                foreach (TypeScriptModule module in topLevelModules)
                {
                    output += module.ToTypeScript(nestingDepth);
                }
            }
            return output;
        }

        #region Module handling

        private Dictionary<string, TypeScriptModule> modulesByNamespace = new Dictionary<string, TypeScriptModule>();

        internal IEnumerable<TypeScriptModule> GetTopLevelModules()
        {
            return modulesByNamespace.Values.Where(mod => mod.IsTopLevel);
        }

        internal TypeScriptModule GetModule(string objectNamespace)
        {
            if (!modulesByNamespace.ContainsKey(objectNamespace))
            {
                string[] objectNamespaceParts = objectNamespace.Split('.');
                TypeScriptModule currentParentModule = null;
                string currentNamespace = string.Empty;

                // step through the namespace
                for (int i = 0; i < objectNamespaceParts.Length; i++)
                {
                    // build the namespace for where we're up to at the moment
                    if (!string.IsNullOrEmpty(currentNamespace))
                    {
                        currentNamespace += ".";
                    }
                    currentNamespace += objectNamespaceParts[i];

                    // if the namespace doesn't exist then create and cache it
                    if (!modulesByNamespace.ContainsKey(currentNamespace))
                    {
                        TypeScriptModule module = new TypeScriptModule(this, currentParentModule, objectNamespaceParts[i]);
                        modulesByNamespace.Add(module.Namespace, module);
                    }
                    currentParentModule = modulesByNamespace[currentNamespace];
                }
            }
            return modulesByNamespace[objectNamespace];
        }

        #endregion

        internal TypeScriptType GetByType<T>()
        {
            string typeName = this.GenerateKey(typeof(T));
            if (!cache.ContainsKey(typeName))
            {
                return null;
            }
            return cache[typeName];
        }

        internal void EnsureInitialised()
        {
            int loopCount = 0;
            bool allInitialised = false;
            do
            {
                // get all objects with uninitialised properties
                TypeScriptType[] requiringInitialisation = this.cache.Values.Where(obj => obj.AllMembers.Any(prop => !prop.IsInitialised)).ToArray();
                if (!requiringInitialisation.Any())
                {
                    allInitialised = true;
                }
                foreach (TypeScriptType obj in requiringInitialisation)
                {
                    TypeScriptProperty[] propertiesRequiringInitialisation = obj.AllMembers.Where(prop => !prop.IsInitialised).ToArray();
                    foreach (TypeScriptProperty prop in propertiesRequiringInitialisation)
                    {
                        prop.Initialise();
                    }
                }
                loopCount++;
            } while (!allInitialised && loopCount < 100);

            if (!allInitialised)
            {
                throw new Exception("The object hierarchy is nested too deep, or more likely there's a circular reference in the serialisation");
            }
        }
    }

}
